package com.mbyte.easy.support.es.template.impl;

import com.alibaba.fastjson.JSON;
import com.mbyte.easy.admin.entity.Draft;
import com.mbyte.easy.support.es.constants.EsConstants;
import com.mbyte.easy.support.es.index.ElasticsearchIndex;
import com.mbyte.easy.support.es.template.ElasticsearchTemplate;
import com.mbyte.easy.support.es.util.*;
import com.mbyte.easy.util.ReflectUtil;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.*;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.*;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.core.CountRequest;
import org.elasticsearch.client.core.CountResponse;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.reindex.BulkByScrollResponse;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;


/**
 * @Author:lxt
 * @Description:to do
 * @Date:2019/10/9
 * @Version:1.0
 */
@Component
@Slf4j
public class ElasticsearchTemplateImpl<T, M> implements ElasticsearchTemplate<T, M> {

    @Autowired
    RestHighLevelClient client;
    @Autowired
    ElasticsearchIndex elasticsearchIndex;


    @PostConstruct
    public void initIndex() throws Exception {
        createIndex(Draft.class);
    }


    /**
     * @Title: getClazz
     * @Description: 获取泛型class
     * @Author: lxt
     * @Date: 2019-10-17 15:15
     * @return: java.lang.Class<T>
     * @throws:
     */
    public Class<T> getTClass(){
        return (Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    @Override
    public void createIndex(Class clazz)throws Exception {
        if(!elasticsearchIndex.exists(clazz)) {
            elasticsearchIndex.createIndex(clazz);
        }
    }
    @Override
    public boolean save(T t) throws Exception {
        MetaData metaData = IndexTools.getMetaData(t.getClass());
        String indexname = metaData.getIndexname();
        String id = Tools.getESId(t);
        IndexRequest indexRequest = null;
        if (StringUtils.isEmpty(id)) {
            indexRequest = new IndexRequest(indexname);
        } else {
            indexRequest = new IndexRequest(indexname).id(id);
        }
        String source = JSON.toJSONString(t);
        indexRequest.source(source, XContentType.JSON);
        IndexResponse indexResponse = null;
        indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
        if (indexResponse.getResult() == DocWriteResponse.Result.CREATED) {
            log.info("INDEX CREATE SUCCESS");
        } else if (indexResponse.getResult() == DocWriteResponse.Result.UPDATED) {
            log.info("INDEX UPDATE SUCCESS");
        } else {
            return false;
        }
        return true;
    }

    @Override
    public BulkResponse save(List<T> list) throws Exception {
        if (list == null || list.size() == 0) {
            return null;
        }
        T t = list.get(0);
        MetaData metaData = IndexTools.getMetaData(t.getClass());
        String indexname = metaData.getIndexname();
        BulkRequest rrr = new BulkRequest();
        for (int i = 0; i < list.size(); i++) {
            T tt = list.get(i);
            String id = Tools.getESId(tt);
            rrr.add(new IndexRequest(indexname).id(id)
                    .source(JSON.toJSONString(tt), XContentType.JSON));
        }
        BulkResponse bulkResponse = client.bulk(rrr, RequestOptions.DEFAULT);
        return bulkResponse;
    }

    @Override
    public BulkResponse bulkUpdate(List<T> list) throws Exception {
        if (list == null || list.size() == 0) {
            return null;
        }
        T t = list.get(0);
        MetaData metaData = IndexTools.getMetaData(t.getClass());
        String indexname = metaData.getIndexname();
        BulkRequest rrr = new BulkRequest();
        for (int i = 0; i < list.size(); i++) {
            T tt = list.get(i);
            String id = Tools.getESId(tt);
            rrr.add(new UpdateRequest(indexname,id)
                    .doc(Tools.getFieldValue(tt)));
        }
        BulkResponse bulkResponse = client.bulk(rrr, RequestOptions.DEFAULT);
        return bulkResponse;
    }

    @Override
    public boolean update(T t) throws Exception {
        MetaData metaData = IndexTools.getMetaData(t.getClass());
        String indexname = metaData.getIndexname();
        String id = Tools.getESId(t);
        if (StringUtils.isEmpty(id)) {
            throw new Exception("ID cannot be empty");
        }
        UpdateRequest updateRequest = new UpdateRequest(indexname, id);
        String source = JSON.toJSONString(t);
        updateRequest.doc(source, XContentType.JSON);
//        updateRequest.doc(Tools.getFieldValue(t));
        UpdateResponse updateResponse = null;
        updateResponse = client.update(updateRequest, RequestOptions.DEFAULT);
        if (updateResponse.getResult() == DocWriteResponse.Result.CREATED) {
            log.info("INDEX CREATE SUCCESS");
        } else if (updateResponse.getResult() == DocWriteResponse.Result.UPDATED) {
            log.info("INDEX UPDATE SUCCESS");
        } else {
            return false;
        }
        return true;
    }

    @Override
    public BulkResponse batchUpdate(QueryBuilder queryBuilder, T t, Class clazz, int limitcount, boolean asyn) throws Exception {
        MetaData metaData = IndexTools.getMetaData(t.getClass());
        String indexname = metaData.getIndexname();
        if (queryBuilder == null) {
            throw new NullPointerException();
        }
        if(Tools.getESId(t) == null || "".equals(Tools.getESId(t))) {
            PageSortHighLight psh = new PageSortHighLight(1, limitcount);
            psh.setHighLight(null);
            PageList pageList = this.search(queryBuilder, psh, clazz, indexname);
            if (pageList.getTotalElements() > limitcount) {
                throw new Exception("beyond the limitcount");
            }
            if (asyn) {
                new Thread(() -> {
                    try {
                        batchUpdate(pageList.getList(), indexname, t);
                        log.info("asyn batch finished update");
                    } catch (Exception e) {
                        log.error("asyn batch update fail", e);
                    }
                }).start();
                return null;
            } else {
                return batchUpdate(pageList.getList(), indexname, t);
            }
        }else{
            throw new Exception("批量更新请不要给主键传值");
        }
    }

    private BulkResponse batchUpdate(List<T> list, String indexname, T tot) throws Exception {
        Map map = Tools.getFieldValue(tot);
        BulkRequest rrr = new BulkRequest();
        for (int i = 0; i < list.size(); i++) {
            T tt = list.get(i);
            rrr.add(new UpdateRequest(indexname, Tools.getESId(tt))
                    .doc(map));
        }
        BulkResponse bulkResponse = client.bulk(rrr, RequestOptions.DEFAULT);
        return bulkResponse;
    }


    @Override
    public boolean updateCover(T t) throws Exception {
        return save(t);
    }

    @Override
    public boolean delete(T t) throws Exception {
        MetaData metaData = IndexTools.getMetaData(t.getClass());
        String indexname = metaData.getIndexname();
        String id = Tools.getESId(t);
        if (StringUtils.isEmpty(id)) {
            throw new Exception("ID cannot be empty");
        }
        DeleteRequest deleteRequest = new DeleteRequest(indexname,id);
        DeleteResponse deleteResponse = null;
        deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT);
        if (deleteResponse.getResult() == DocWriteResponse.Result.DELETED) {
            log.info("INDEX DELETE SUCCESS");
        } else {
            return false;
        }
        return true;
    }

    @Override
    public BulkByScrollResponse deleteByCondition(QueryBuilder queryBuilder, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String[] indexname = metaData.getSearchIndexNames();
        DeleteByQueryRequest request = new DeleteByQueryRequest(indexname);
        request.setQuery(queryBuilder);
        BulkByScrollResponse bulkResponse = client.deleteByQuery(request, RequestOptions.DEFAULT);
        return bulkResponse;
    }


    @Override
    public SearchResponse search(SearchRequest searchRequest) throws IOException {
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        return searchResponse;
    }

    @Override
    public List<T> search(QueryBuilder queryBuilder, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String[] indexname = metaData.getSearchIndexNames();
        return search(queryBuilder, clazz, indexname);
    }

    @Override
    public List<T> search(QueryBuilder queryBuilder, Class<T> clazz, String... indexs) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        List<T> list = new ArrayList<>();
        SearchRequest searchRequest = new SearchRequest(indexs);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder);
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(EsConstants.DEFALT_PAGE_SIZE);
        searchRequest.source(searchSourceBuilder);
        if (metaData.isPrintLog()) {
            log.info(searchSourceBuilder.toString());
        }
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = searchResponse.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit : searchHits) {
            T t = JSON.parseObject(hit.getSourceAsString(), clazz);
            ReflectUtil.setValue(t,"hitScore",hit.getScore());
            list.add(t);
        }
        return list;
    }


    @Override
    public List<T> searchWithHitScore(QueryBuilder queryBuilder, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        List<T> list = new ArrayList<>();
        SearchRequest searchRequest = new SearchRequest(metaData.getSearchIndexNames());
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder);
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(EsConstants.DEFALT_PAGE_SIZE);
        searchRequest.source(searchSourceBuilder);
        if (metaData.isPrintLog()) {
            log.info(searchSourceBuilder.toString());
        }
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = searchResponse.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit : searchHits) {
            T t = JSON.parseObject(hit.getSourceAsString(), clazz);
            ReflectUtil.setValue(t,"hitScore",hit.getScore());
            list.add(t);
        }
        return list;
    }

    @Override
    public T searchOne(QueryBuilder queryBuilder, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        SearchRequest searchRequest = new SearchRequest(metaData.getIndexname());
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder);
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(1);
        searchRequest.source(searchSourceBuilder);
        if (metaData.isPrintLog()) {
            log.info(searchSourceBuilder.toString());
        }
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = searchResponse.getHits();
        SearchHit[] searchHits = hits.getHits();
        if(searchHits != null && searchHits.length > 0){
            T t = JSON.parseObject(searchHits[0].getSourceAsString(), clazz);
            return t;
        }
        return null;
    }

    @Override
    public List<T> searchMore(QueryBuilder queryBuilder,int limitSize, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String[] indexname = metaData.getSearchIndexNames();
        return searchMore(queryBuilder,limitSize,clazz,indexname);
    }

    @Override
    public List<T> searchMore(QueryBuilder queryBuilder,int limitSize, Class<T> clazz, String... indexs) throws Exception {
        PageSortHighLight pageSortHighLight = new PageSortHighLight(1, limitSize);
        PageList pageList = search(queryBuilder, pageSortHighLight, clazz, indexs);
        if(pageList != null){
            return pageList.getList();
        }
        return null;
    }

    @Override
    public long count(QueryBuilder queryBuilder, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String[] indexname = metaData.getSearchIndexNames();
        return count(queryBuilder, clazz, indexname);
    }

    @Override
    public long count(QueryBuilder queryBuilder, Class<T> clazz, String... indexs) throws Exception {
        CountRequest countRequest = new CountRequest(indexs);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder);
        countRequest.source(searchSourceBuilder);
        CountResponse countResponse = client.count(countRequest, RequestOptions.DEFAULT);
        long count = countResponse.getCount();
        return count;
    }

    @Override
    public T getById(M id, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String indexname = metaData.getIndexname();
        if (StringUtils.isEmpty(id)) {
            throw new Exception("ID cannot be empty");
        }
        GetRequest getRequest = new GetRequest(indexname,id.toString());
        GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
        if (getResponse.isExists()) {
//            return JsonUtils.string2Obj(getResponse.getSourceAsString(), clazz);
            return JSON.parseObject(getResponse.getSourceAsString(), clazz);
        }
        return null;
    }

    @Override
    public List<T> mgetById(M[] ids, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String indexname = metaData.getIndexname();
        MultiGetRequest request = new MultiGetRequest();
        for (int i = 0; i < ids.length; i++) {
            request.add(new MultiGetRequest.Item(indexname, ids[i].toString()));
        }
        MultiGetResponse response = client.mget(request, RequestOptions.DEFAULT);
        List<T> list = new ArrayList<>();
        for (int i = 0; i < response.getResponses().length; i++) {
            MultiGetItemResponse item = response.getResponses()[i];
            GetResponse getResponse = item.getResponse();
            if (getResponse.isExists()) {
                list.add(JSON.parseObject(getResponse.getSourceAsString(), clazz));
//                list.add(JsonUtils.string2Obj(getResponse.getSourceAsString(), clazz));
            }
        }
        return list;
    }

    @Override
    public boolean exists(M id, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String indexname = metaData.getIndexname();
        if (StringUtils.isEmpty(id)) {
            throw new Exception("ID cannot be empty");
        }
        GetRequest getRequest = new GetRequest(indexname, id.toString());
        GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
        if (getResponse.isExists()) {
            return true;
        }
        return false;
    }



    @Override
    public boolean deleteById(M id, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String indexname = metaData.getIndexname();
        if (StringUtils.isEmpty(id)) {
            throw new Exception("ID cannot be empty");
        }
        DeleteRequest deleteRequest = new DeleteRequest(indexname, id.toString());
        DeleteResponse deleteResponse = null;
        deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT);
        if (deleteResponse.getResult() == DocWriteResponse.Result.DELETED) {
            log.info("INDEX DELETE SUCCESS");
        } else {
            return false;
        }
        return true;
    }

    @Override
    public PageList<T> search(QueryBuilder queryBuilder, PageSortHighLight pageSortHighLight, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String[] indexname = metaData.getSearchIndexNames();
        if (pageSortHighLight == null) {
            throw new NullPointerException("PageSortHighLight不能为空!");
        }
        return search(queryBuilder, pageSortHighLight, clazz, indexname);
    }

    @Override
    public PageList<T> search(QueryBuilder queryBuilder, PageSortHighLight pageSortHighLight, Class<T> clazz, String... indexs) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        PageList<T> pageList = new PageList<>();
        List<T> list = new ArrayList<>();
        if (pageSortHighLight == null) {
            throw new NullPointerException("PageSortHighLight不能为空!");
        }
        SearchRequest searchRequest = new SearchRequest(indexs);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder);
        //分页
        searchSourceBuilder.from((pageSortHighLight.getCurrentPage() - 1) * pageSortHighLight.getPageSize());
        searchSourceBuilder.size(pageSortHighLight.getPageSize());
        //排序
        Sort sort = pageSortHighLight.getSort();
        List<Sort.Order> orders = sort.listOrders();
        orders.forEach(order ->
                searchSourceBuilder.sort(new FieldSortBuilder(order.getProperty()).order(order.getDirection()))
        );
        //高亮
        HighLight highLight = pageSortHighLight.getHighLight();
        boolean highLightFlag = false;
        if (highLight != null && highLight.getHighLightList() != null && highLight.getHighLightList().size() != 0) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            if (!StringUtils.isEmpty(highLight.getPreTag()) && !StringUtils.isEmpty(highLight.getPostTag())) {
                highlightBuilder.preTags(highLight.getPreTag());
                highlightBuilder.postTags(highLight.getPostTag());
            }
            for (int i = 0; i < highLight.getHighLightList().size(); i++) {
                highLightFlag = true;
                // You can set fragment_size to 0 to never split any sentence.
                //不对高亮结果进行拆分
                highlightBuilder.field(highLight.getHighLightList().get(i), 0);
            }
            searchSourceBuilder.highlighter(highlightBuilder);
        }
        searchRequest.source(searchSourceBuilder);
        if (metaData.isPrintLog()) {
            log.info(searchSourceBuilder.toString());
        }
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = searchResponse.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit : searchHits) {
            String str1 = hit.getSourceAsString();
            Map<String,Object> str2 = hit.getSourceAsMap();
//            T t = JsonUtils.string2Obj(hit.getSourceAsString(), clazz);
            T t = JSON.parseObject(hit.getSourceAsString(), clazz);
            //替换高亮字段
            if (highLightFlag) {
                Map<String, HighlightField> hmap = hit.getHighlightFields();
                hmap.forEach((k, v) ->
                        {
                            try {
                                Object obj = mapToObject(hmap, clazz);
                                BeanUtils.copyProperties(obj, t, BeanTools.getNoValuePropertyNames(obj));
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                );
            }
            list.add(t);
        }

        pageList.setList(list);
        pageList.setTotalElements(hits.getTotalHits().value);
        pageList.setTotalPages(getTotalPages(hits.getTotalHits().value, pageSortHighLight.getPageSize()));
        return pageList;
    }

    private Object mapToObject(Map map, Class<?> beanClass) throws Exception {
        if (map == null) {
            return null;
        }
        Object obj = beanClass.newInstance();
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (map.get(field.getName()) != null && !StringUtils.isEmpty(map.get(field.getName()))) {
                int mod = field.getModifiers();
                if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
                    continue;
                }
                field.setAccessible(true);
                if (map.get(field.getName()) instanceof HighlightField && ((HighlightField) map.get(field.getName())).fragments().length > 0) {
                    field.set(obj, ((HighlightField) map.get(field.getName())).fragments()[0].string());
                }
            }
        }
        return obj;
    }


    @Override
    public List<T> scroll(QueryBuilder queryBuilder, Class<T> clazz, long time, String... indexs) throws Exception {
        if (queryBuilder == null) {
            throw new NullPointerException();
        }
        String[] indexname = indexs;
        List<T> list = new ArrayList<>();
        Scroll scroll = new Scroll(TimeValue.timeValueHours(time));
        SearchRequest searchRequest = new SearchRequest(indexname);
        searchRequest.scroll(scroll);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder);
        searchSourceBuilder.size(EsConstants.DEFAULT_SCROLL_PERPAGE);
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        String scrollId = searchResponse.getScrollId();
        SearchHit[] searchHits = searchResponse.getHits().getHits();
        //第一把查询出的结果
        for (SearchHit hit : searchHits) {
//            T t = JsonUtils.string2Obj(hit.getSourceAsString(), clazz);
            T t = JSON.parseObject(hit.getSourceAsString(), clazz);
            list.add(t);
        }
        while (searchHits != null && searchHits.length > 0) {
            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
            scrollRequest.scroll(scroll);
            searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
            scrollId = searchResponse.getScrollId();
            searchHits = searchResponse.getHits().getHits();
            for (SearchHit hit : searchHits) {
//                T t = JsonUtils.string2Obj(hit.getSourceAsString(), clazz);
                T t = JSON.parseObject(hit.getSourceAsString(), clazz);
                list.add(t);
            }
        }
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        ClearScrollResponse clearScrollResponse
                = client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
        boolean succeeded = clearScrollResponse.isSucceeded();
        return list;
    }


    @Override
    public List<T> scroll(QueryBuilder queryBuilder, Class<T> clazz) throws Exception {
        MetaData metaData = IndexTools.getMetaData(clazz);
        String[] indexname = metaData.getSearchIndexNames();
        return scroll(queryBuilder, clazz, EsConstants.DEFAULT_SCROLL_TIME, indexname);
    }


    private int getTotalPages(long totalHits, int pageSize) {
        return pageSize == 0 ? 1 : (int) Math.ceil((double) totalHits / (double) pageSize);
    }


}
